#!/usr/bin/env ruby


class FakeSchema
  
  # emulation of schema definition method
  #
  module ActiveRecord
    class Schema
      def self.define(options={}, &blk)
        yield
      end
    end
  end
  
  def initialize(schema_code)
    @tables = {}
    eval(schema_code)
  end
  
  # yields a table
  def create_table(name, options={})
    #puts name + '-----------------'
    @tables[name] = {
      :options => options,
      :columns => {}
    }
    table_hash = @tables[name]
    
    # add a singleton method to catch column creation
    #
    def table_hash.column(name, type, options={})
      #puts '  |-> ' + name.to_s + ' ' + type.to_s
      self[:columns][name] = {
        :options => options,
        :type => type
      }
      nil
    end
    
    yield( @tables[name] )
  end
  
  def tables
    @tables
  end
  
  # build a migration from the differances
  # between two schema outlines
  #
  def diff(other_schema)
    # vars to hold migration code
    up = ""
    down = ""
    
    # list of tables that are to be ignored
    ignored_tables = []
    ignored_columns = []
    
    # check for tables that have been CREATED 
    #
    other_schema.tables.reject{|k,v| @tables.keys.include?(k) }.each do |table_name, table_details|
      ignored_tables << table_name
      # up
      up << %{create_table "#{table_name}", #{table_details[:options].inspect} do |t|\n}
      table_details[:columns].each do |name,info| 
        up << %{  t.column "#{name}", :#{info[:type]}}
        up << ", #{info[:options].inspect}" unless info[:options].empty?
        up << "\n"
      end
      up << "end\n"
      # down
      down << %{drop_table "#{table_name}" \n}
    end
    
    # check for tables that have been DROPPED
    #
    @tables.reject{|k,v| other_schema.tables.keys.include?(k) }.each do |table_name, table_details|
      ignored_tables << table_name
      # up
      up << %{drop_table "#{table_name}"}
      # down
      down << %{\ncreate_table "#{table_name}", #{table_details[:options].inspect} do |t|\n}
      table_details[:columns].each do |name,info| 
        down << %{  t.column "#{name}", :#{info[:type]}}
        down << ", #{info[:options].inspect}" unless info[:options].empty?
        down << "\n"
      end
      down << "end\n"
    end
    
    
    # check for columns that have been REMOVED
    #
    @tables.reject{|k,v| ignored_tables.include?(k)}.each do |table_name, table_details|
      table_details[:columns].reject{|k,v| other_schema.tables[table_name][:columns][k]}.each do |col_name, col_details|
        ignored_columns << "#{table_name}##{col_name}"
        # up
        up << %{remove_column "#{table_name}", "#{col_name}"\n}
        # down
        down << %{add_column "#{table_name}", "#{col_name}", :#{col_details[:type]}}
        down << ", #{col_details[:options].inspect}" unless col_details[:options].empty?
        down << "\n"
      end
    end
    
    # check for columns that have been ADDED
    #
    other_schema.tables.reject{|k,v| ignored_tables.include?(k)}.each do |table_name, table_details|
      table_details[:columns].reject{|k,v| @tables[table_name][:columns][k]}.each do |col_name, col_details|
        ignored_columns << "#{table_name}##{col_name}"
        # up
        up << %{add_column "#{table_name}", "#{col_name}", :#{col_details[:type]}}
        up << ", #{col_details[:options].inspect}" unless col_details[:options].empty?
        up << "\n"
        # down
        down << %{remove_column "#{table_name}", "#{col_name}"\n}
      end
    end
    
    # check for columns that have be ALTERED
    #
    other_schema.tables.reject{|k,v| ignored_tables.include?(k)}.each do |table_name, table_details|
      table_details[:columns].reject{|k,v| ignored_columns.include?("#{table_name}##{k}") or (@tables[table_name][:columns][k][:options]==v[:options] && @tables[table_name][:columns][k][:type]==v[:type])}.each do |col_name, col_details|
        # up
        up << %{change_column "#{table_name}", "#{col_name}", :#{col_details[:type]}, #{col_details[:options].inspect}\n}
        # down
        down << %{change_column "#{table_name}", "#{col_name}", :#{@tables[table_name][:columns][col_name][:type]}, #{@tables[table_name][:columns][col_name][:options].inspect}\n}
      end
    end
    
    # return
%{
class SchemaEvolution004 < ActiveRecord::Migration
  def self.up
#{up.gsub(/^/,'    ')}  end
  
  def self.down
#{down.gsub(/^/,'    ')}  end
end
}
  end
  
end

    
class SchemaDiff
  
  
  def initialize(older_schema, newer_schema='')
    @older_schema = FakeSchema.new(older_schema)
    @newer_schema = FakeSchema.new(newer_schema)
  end
  
  def to_s
    @older_schema.diff(@newer_schema)
  end
  
end

schema_1 = %{
  ActiveRecord::Schema.define do
    create_table "assigned_sections", :force => true do |t|
      t.column "article_id", :integer
      t.column "position",   :integer, :default => 1
    end
  end
}
schema_2 = %{
  ActiveRecord::Schema.define do
    create_table "assigned_sections", :force => true do |t|
      t.column "article_id", :integer
      t.column "section_id", :integer
      t.column "position",   :string, :default => 2
    end
  end
}

# should produce :
#
# class AssignedSections001 
#   def self.up
#     add_column "assigned_sections", "section_id", :integer
#   end
#
#   def self.down
#     remove_column "assigned_sections", "section_id", :integer
#   end
# end

# STDOUT.puts "hash diff---------------"
# STDOUT.puts( {:boo => {:this => 1, :why => 4}, :fine_table => {:col => 'ok'}}.diff({:fine_table => {:col => 'ok'}, :boo => {:this => 2, :why => 4}}).inspect )
# STDOUT.puts "schema diff---------------"
STDOUT.puts SchemaDiff.new(schema_1, schema_2).to_s

 